本博是吴恩达DeepLearning.ai 的学习笔记
改善深层神经网络: 超参调试、正则化以及优化
训练集 开发集 测试集 的划分
假设所有的训练数据如下
一个典型的数据划分:
即:一部分作为 训练集
一部分作为 简单交叉验证集/验证集
一部分作为 测试集
具体的流程是 在训练集上 对各模型运行训练算法 将训练完成的模型 带入 交叉验证集 选择出最佳模型
然后带入测试集对神经网络做出无偏评估
小数据量的时候,一般会将数据划分为 训练集(70%) 测试集(30%)或者训练集(60%)、验证集(20%、测试集(20%)。
在大数据量(百万级)时,验证集 测试集的数据量和小数据量的情况差不多,仍然可能需要成千上万,但是相比百万级的训练集来说,测试集所占的比例理所当然的变小了。比如我们拥有1000000,这么多的数据,训练集:验证集:测试集=98:1:1。
保证 验证集合测试集具有相同的分布
有时候 训练集和测试集的数据来源往往不同,比如说 训练集可能是通过爬虫爬取的网页图片,但是测试集是来自用户通过app上传的图片,这些图片的质量和对齐方式参差不齐,这种情况 最好能保证 测试集和交叉验证集具有相同的分布。
为什么要保证分布相同
我们先从测试集和验证集的作用开始说起
当我们在拿到数据后,会把数据划分为三部分,将训练集丢给模型,这个步骤是为了进行梯度下降,以期得到模型参数。
然后拿到训练完成的模型,带入验证集,这个部分是为了检查模型是否能够很好的拟合验证数据,因为这部分数据是没有经过梯度下降的,可以说验证集和测试集没有交
集,测试的准确率是可靠的。
但是模型除了普通参数(w和b)之外,还有超参数的存在,当不引入强化学习的情况下,普通参数可以被梯度下降更新,也就是可以被训练集更新,但是为了提高模型性能,我们往往会对 神经网络层数、网络节点数、迭代次数、学习步长等进行调整,这些参数不受梯度下降的影响,一般都是根据验证集的表现情况进行人为调整。
所以可以说,验证集也对学习结果产生了影响,所以需要一份完全没有经过学习影响的数据,来评估最终模型的表现情况,这个就是测试集存在的意义。
为什么要保证测试集和验证集有相同的分布呢?
因为一旦定义好了测试集和验证集,开发人员的目的就是专注提高验证集的表现,这便要求验证集的选取可以分布均匀,可以体现核心任务。如果验证集和测试集分布不同,就可能导致 系统在验证集上表现良好,在测试集表现不好。这种情况可能会有多种原因:
- 算法在开发集上过拟合了。
- 测试集比开发集更难进行预测,尽管算法做得足够好了,却很难有进一步的提升空间。
- 测试集不一定更难预测,但它与开发集性质并不相同(分布不同)。
因此在开发集上表现良好的算法不一定在测试集上也能够取得出色表现。
这样就引入了新的不确定性–提高算法在验证集的表现,是否能提高其在测试集的表现?如果是这种情况,大量针对开发集性能的改进工作将会是徒劳的。
如何评估模型表现? 偏差、方差
偏差(bias):可以理解为 模型在训练集的表现不佳 也就是模型无法很好的拟合数据 称之为欠拟合
方差(variance):模型在训练集表现良好,但在测试集表现差,模型过于拟合了训练集数据导致无法正确反映数据规律,称之为 过拟合
在实际中,判断偏差和方差一般会通过 训练集和测试集误差来判断
从左到右:
- 高方差:模型过拟合了
- 高偏差:模型没有被很好的训练 但是貌似没有过拟合
- 高方差 高偏差: 模型没有很好拟合数据 同时在测试集表现也不佳 有时有些高维数据中会出现这种情况,就是在某些区域的偏差高,有些区域的方差高
- 低方差 低偏差: 模型可以很好表现数据特征
即:
训练集误差高 表示 偏差大
测试集误差高 表示 方差大
关于方差和偏差的数学原理 ,见文章附录[1]。
一个基本流程
1 | st=>start: 获取数据 |
在早期机器学习的时代,我们没有太多工具可以只影响偏差或者方差的一种而不对另一种造成影响,所以往往需要在两者间权衡。
现在在大数据时代,我们有了一些工具,比如只要对数据进行适当正则,再构建一个更大的神经网络,就可以在几乎不影响方差的情况下,减少偏差。而采用更多数据,或者对数据正则化,可以在不过多影响偏差的情况下,减少方差
降低方差的手段:正则化
出现高方差往往就表示,模型对数据过拟合了。
前面讲到 降低方差的手段,可以通过获取更多数据或者对数据进行正则来实现。
更多数据的获取,实现起来成本比较高,那正则化又为什么可以减少方差呢?
如何实施正则化
logistic 回归
我们之前定义的损失函数为
现在在其基础上加上正则化参数 也就是向量w的二范数(也有使用一范数的,但目前更多倾向于使用二范数)
至于b的正则化参数,可加可不加,因为w通常是一个高维度的矩阵,它已经包含很多参数了,b只是众多参数的其中之一,加不加影响也不是很大。
其中 λ是正则化参数。这个参数需要我们在验证集经过多次调试来选择最佳取值,λ属于超参数的一种。
神经网络
神经网络和logistic回归的差别在于,神经网络是有多层隐藏神经元的数学模型。
它的正则化参数则是将所有层的w向量写成一个矩阵后 矩阵W的二范数,在DL中,习惯将矩阵的二范数称之为 F范数,或者弗罗贝尼乌斯范数
在之前的梯度下降过程中,我们通过反向传播 计算出了原始J对w[l]的导数:
现在代价函数J增加了一项正则化参数
对正则化参数求导
为
则新的dw[l]为
带入w[l] = w[l]-α*dw[l]
得到
可以看到,如果将w[l]提取出来,每次反向传播后,更新的权重值相当于在其前面乘上了这么一个参数:
显而易见这个参数是小于1的。
所以L2正则化也被称之为 权重衰减
所以 对神经网络 应用L2 正则化的过程 其实相当于在每次反向传播更新梯度的过程中,对w[l]乘上了一个小于1的参数1-αλ/m
这个参数是小于1的,所以 L2正则化 也被称之为 权重衰减。
为什么正则化可以减少过拟合
从直观上理解,因为我们的代价函数加上了对权重矩阵W的正则化参数,也就是W的F范数,在训练过程中,为了降低代价函数J,必然会压缩正则化参数,当λ设置的足够大,会导致矩阵W的F范数接近于0,也就意味着W中的很多元素变为了0,
等于原始神经网络中的很多神经元失去了作用,模型也被精简了。
此时模型越来越趋近于logistic回归
模型过于简单的时候,会导致偏差变高,模型甚至无法很好的拟合数据。
但这种W中参数变为0的情况在现实情况下一般不会发生,往往是某些w会变得很小,等于在训练过程中,这些神经元没有消失,只不过权重值变得很小而已。这样来看,貌似神经网络变得更简单了,这样更不容易出现过拟合。
下面举一个直观一些的例子。
假设每一层的激活函数都是tanh(),tanh()函数具有一个特殊性质,就是当x范围比较小的时候,tanh()接近y=x。
当我们对神经网络施加L2正则化,在训练中会导致w变小,又因为g(z)中的z 等于
当w[l]变小,会导致z[l]变小,当z落在tanh的中间那一小部分范围时,整个神经网络只利用到了tanh的线性部分,模型也会趋近于线性回归。这样会有效降低过拟合出现的情况
另一种正则化方式: Dropout正则化
Dropout,也被称之为随机失活。
其实就是 令神经网络的每一层的每个神经元,按照一定的概率失活。
我们将原始训练集分为若干子集,对每一个子集,都利用一次dropout
假设在这个神经网络中,我们设置每个元素的失活概率为0.5,那么在这个子集中,我们便会得到一个精简的神经网络
对每一个训练子集机型dropout,便会得到很多精简的神经网络,我们对每一个神经网络进行训练。其实这里也能看出来,为什么dropout可以有效防止过拟合,以为实施过dropout的神经网络变小了。
下面以python代码示例
1 | import numpy as np |
关于为什么a3要除以keep-prop
这被称为inverted dropout。当模型使用了dropout layer,训练的时候只有占比为P 的隐藏层单元参与训练,那么在预测的时候,如果所有的隐藏层单元都需要参与进来,则得到的结果相比训练时平均要大1/p ,为了避免这种情况,就需要测试的时候将输出结果乘以 p 使下一层的输入规模保持不变。而利用inverted dropout,我们可以在训练的时候直接将dropout后留下的权重扩大1/p 倍,这样就可以使结果的scale保持不变,而在预测的时候也不用做额外的操作了,更方便一些。
或者更直观的解释
10个人拉一个10吨车,第一次(训练时),只有5个人出力(有p=0.5的人被dropout了),那么这5个人每个人出力拉2吨。第二次(预测时),10个人都被要求出力,这次每个人出的力就是2*(1-0.5)=1吨了 该解释转自知乎
keep-prop的数值,对于不同的层,可以根据需要设置不同大小
第二层的权重矩阵w[7×7],占比最多,为了减少过拟合,可以将第二层的keep-prop设置的低一些,在4,5层 可以将keep_props设置的高一些。
总结一下:
如果你觉得 某一层相比其它层更容易出现过拟合,那么可以将该层的keep-prop设置的低一些,一般不会输入层施加drop-out,就算是施加,也尽量是keep-prop接近1。
drop-out是对神经网络施加正则化的一种手段,它强迫模型不过度依赖于某一个神经元,使得权重在神经元之间平均分散,它可以使权重矩阵的F范数变小,但是会导致我们无法debug,因为施加drop-out之后,代价函数J便没有了一个确切的定义,我们无法得到一个单调递减的代价函数J,如果在某次迭代中出现了问题,也无法定位,因为每次的模型都是随机产生的。
drop-out的作用是为了解决过拟合,所以如果模型不存在过拟合情况,可以不用,一般来说 在计算机视觉领域,drop-out会适用的比较频繁,因为CV的输入一般都是图片像素,因为输入太大了,导致数据量相比输入而言总显得过小,很容易出现过拟合的情况,所以drop-out在CV领域,基本是一个默认选项。
dropout的优缺点
Dropout优点
计算方便。训练过程中使用Dropout产生 n 个随机二进制数与状态相乘即可。每个样本每次更新的时间复杂度: O(n),空间复杂度: O(n)。
适用广。Dropout不怎么限制适用的模型或训练过程,几乎在所有使用分布式表示且可以用随机梯度下降训练的模型上都表现很好。包括:前馈神经网络、概率模型、受限波尔兹曼机、循环神经网络等。
相比其他正则化方法(如权重衰减、过滤器约束和稀疏激活)更有效。也可与其他形式的正则化合并,得到进一步提升。
Dropout缺点
不适合宽度太窄的网络。否则大部分网络没有输入到输出的路径。
不适合训练数据太小(如小于5000)的网络。训练数据太小时,Dropout没有其他方法表现好。
不适合非常大的数据集。数据集大的时候正则化效果有限(大数据集本身的泛化误差就很小),使用Dropout的代价可能超过正则化的好处
其他的正则化方法
通过处理输入 比如对输入图片进行 翻转 剪裁 对数字进行扭曲 来人为增大数据集 减少误差
early-stop
在 testSet的误差开始变大的时候,及时停止
正则化输入
假设输入的数据集,每一个输入有两个特征,则输入的散点图如下
归一化输入 则需要两部
- 零均值化: 计算输入的均值μ,则
X = X-μ
这样的目的是让输入值的均值为0
- 归一化方差
可以看到 零均值化后的输入 x1 和x2方向的方差差距还是挺大的
通过计算所有数据的方差σ² = 1/m*Σ(x(i)^2)
再将所有向量除以σ,x1和 x2的方差就都变为1了
注意: 测试集和训练集请使用同样的归一化参数
为什么要归一化输入
假设没有对参数做归一化处理
那么x1 和x2的输入范围可能差距非常大,假设x1取值[0,10000] 而x2 取值[0,1] 这样导致的结果是参数w1 和 w2 的范围差距同样非常大,这样会导致J[W,B] 的图像变得狭长,会导致梯度下降的速度变慢[注2]
当对输入进行归一化处理后,图像更偏向与均匀的碗形。
这样对代价函数J进行梯度下降时,会使得梯度下降的速度更快。
[注2]:
梯度下降的原理是让w和b 沿着 j对w和b 的负梯度方向前进,知道接近全局最小值。
让图像形状狭长的时候,梯度的前进方向会更容易偏离最优值的方向,那么在梯度下降过程中,前进方向难免会有很多折线,这样会使的前进速度变慢。 当图像偏向更均匀的圆形时,前进方向更平缓,也更容易找到最优解。
梯度消失与梯度爆炸
深层网络由许多非线性层堆叠而来,每一层非线性层都可以视为是一个非线性函数 f(x)···f(x)f(x)(非线性来自于非线性激活函数),因此整个深度网络可以视为是一个复合的非线性多元函数
我们最终的目的是希望这个多元函数可以很好的完成输入到输出之间的映射,假设不同的输入,输出的最优解是g(x) g(x)g(x) ,那么,优化深度网络就是为了寻找到合适的权值,满足Loss=L(g(x),F(x)) Loss = L(g(x),F(x))Loss=L(g(x),F(x))取得极小值点,比如最简单的损失函数
发生梯度爆炸、梯度消失的原因有两个
网络层级太深
采用了不合适的激活函数
例如,对于下图所示的含有3个隐藏层的神经网络,梯度消失问题发生时,接近于输出层的hidden layer 3等的权值更新相对正常,但前面的hidden layer 1的权值更新会变得很慢,导致前面的层权值几乎不变,仍接近于初始化的权值,这就导致hidden layer 1相当于只是一个映射层,对所有的输入做了一个同一映射,这是此深层网络的学习就等价于只有后几层的浅层网络的学习了。
以下图的反向传播为例(假设每一层只有一个神经元且对于每一层
可以推导出
而sigmod的导数
最大值为1/4 ,而初始化网络权值的时候,w通常小于1 ,导致
因此对于上面的链式求导,层数越多,求导结果越小,因而导致梯度消失的情况出现。梯度爆炸的原因正好相反,
其实梯度爆炸和梯度消失问题都是因为网络太深,网络权值更新不稳定造成的,本质上是因为梯度反向传播中的连乘效应
合理初始化神经网络的权重值(Xavier初始化)
使用不同的激活函数,对于参数矩阵w的初始化方法也不相同
具体可以看这篇文章,详细阐述了Xavier的初始化原理。
梯度逼近
使用双边公差来计算梯度,可以有效减少误差
梯度检验
将所有w和b 排列为一个超级大的向量θ,对θ的每一项θi,依次执行梯度逼近,计算出每一项dθiapprox
检查
是否在合理范围内。
比如 如果ξ取值为10^-7, 梯度检验的结果和该标准相差几个数量级,就要考虑算法中是不是出现了bug。
具体到例子中,代价函数关于参数的梯度的定义:
【这里wi 和 θi是 一一对应的】
我们想要确保 ∂J/∂wi 的计算是正确的,只需要取 ε 为一个很小的数 (例如 10^−7),然后计算 (J(wi+ε)−J(wi−ε))/2ε 是否约等于 ∂J/∂wi,实际操作中判断两个参数向量的欧氏距离是否足够小。
对于每个参数 wi,实施梯度检验的步骤一般是:
1 . 使用前向传播计算代价函数 J(wi+ε)
2 . 使用前向传播计算代价函数 J(wi−ε)
3 . 计算梯度的近似值 (数值微分) gradapprox[i]=(J(wi+ε)−J(wi−ε))/2ε
- 使用链式法则计算反向传播梯度,缓存到变量 grad 中
使用以下公式计算梯度 grad 和梯度的近似值 gradapprox 的欧氏距离:
difference=∣∣grad−gradapprox∣∣2 / ∣∣grad∣∣2+∣∣gradapprox||2
实施梯度检验,有是哪个基本原则:
不要再训练中使用梯度检验
梯度检验只适用于调试,不要在训练中打开它
因为对每一个θ做数值逼近是非常消耗时间的,只在调试bug的时候打开它,在训练的时候关闭。
如果梯度检验失败,要检查每一项dθiapprox的值。
这样可以发现,是在求导哪一个参数的过程中出了问题
如果代价函数有正则化项,求解dθ的时候不要漏掉这个正则化项
dropout不可以和梯度检验同时使用
因为每次迭代过程中 Dropout 会使神经元结点随机失活,难以计算 Dropout 在梯度下降上的代价函数 J。
实施梯度检验的python代码:
1 | def gradient_check_n(parameters, gradients, X, Y, epsilon=1e-7): |